{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 2\n",
    "\n",
    "### <center> Автор материала: Дмитрий Кустиков (@waniz)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данной работе предлагаю рассмотреть достаточно известный датасет (он был симулирован, но достаточно мал и показателен):\n",
    "\n",
    "Human Resources Analytics (https://www.kaggle.com/ludobenistant/hr-analytics/downloads/HR_comma_sep.csv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import seaborn as sns\n",
    "import xgboost as xgb\n",
    "import catboost as cat\n",
    "import itertools\n",
    "\n",
    "from matplotlib import pyplot as plt\n",
    "\n",
    "from sklearn import preprocessing\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.model_selection import StratifiedKFold\n",
    "from sklearn.model_selection import cross_val_score, cross_val_predict\n",
    "from sklearn.model_selection import learning_curve\n",
    "from sklearn.metrics import roc_auc_score, confusion_matrix, auc, roc_curve\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.model_selection import learning_curve\n",
    "from sklearn.model_selection import ShuffleSplit\n",
    "\n",
    "\n",
    "from mlxtend.feature_selection import SequentialFeatureSelector as SFS\n",
    "\n",
    "from hyperopt import fmin, hp, STATUS_OK, Trials, tpe\n",
    "\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Процесс сбора данных:__\n",
    "\n",
    "Датасет был сгенерирован."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Описание решаемой задачи и ее ценность:__\n",
    "\n",
    "Предотвратить уход сотрудников из компании, определить причины.\n",
    "\n",
    "Определить какие признаки влияют на то, что сотрудник уволится (профессиональное выгорание, отсутствие повышения и прочее). Датасет актуален для превентивного анализа ситуации в компании и сохранения ценных кадров. В нем содержаться основные количественные характеристики сотрудников и их работы в компании.\n",
    "\n",
    "Описанная задача является задачей __бинарной классификации__."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "__Описание признаков и целевой переменной:__\n",
    "\n",
    "    - left                 - целевая переменная - ушел ли сотрудник из компании;\n",
    "    - satisfaction_level   - уровень удовлетворенности (0-1);\n",
    "    - last_evaluation      - время, прошедшее с последнего повышения (в годах);\n",
    "    - number_project       - количество законченных проектов за все время работы;\n",
    "    - average_montly_hours - среднее количество часов работы в месяц;\n",
    "    - time_spend_company   - количество лет, которые сотрудник проработал в компании;\n",
    "    - work_accident        - были ли особые случае (несчастный случай на работе и прочее);\n",
    "    - promotion_last_5years- было ли повышение за последние 5 лет;\n",
    "    - sales                - департамент сотрудника;\n",
    "    - salary               - оценочный уровень зарплаты (градации).\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = pd.read_csv('HR_comma_sep.csv')\n",
    "dataset.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 2. Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим наличие пропущенных значений в датасете:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.isnull().sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим основную статистическую информацию:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Наша целевая переменная left, посмотрим на ее распределение:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(6, 4))\n",
    "plt.scatter(range(dataset.shape[0]), np.sort(dataset.left.values))\n",
    "plt.xlabel('index', fontsize=12)\n",
    "plt.ylabel('left', fontsize=12)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим, какие типы признаков у нас есть в датасете:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dtype_df = dataset.dtypes.reset_index()\n",
    "dtype_df.columns = [\"Count\", \"Column Type\"]\n",
    "dtype_df.groupby(\"Column Type\").aggregate('count').reset_index()\n",
    "\n",
    "dtype_df.loc[:10,:]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В наличии у нас 2 категориальные переменные (sales и salary), остальные являются количественными."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 3. Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Посмотрим на матрицу корреляции для датасета:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "corr = dataset.corr()\n",
    "plt.figure(figsize=(12, 8))\n",
    "sns.heatmap(corr, annot=True, cmap=\"YlGnBu\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, сильно корреляции между признаками нет (< 0.7). Считаем, что они линейнонезависимы.\n",
    "\n",
    "__Пропущенные значения и выбросы отсутствуют (согласно гистрограммам)__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Распределения для признаков имеют вид:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for column in ['satisfaction_level', 'last_evaluation', 'number_project', \n",
    "               'average_montly_hours', 'time_spend_company']:\n",
    "    plt.figure(figsize=(8, 6))\n",
    "    plt.title(column)\n",
    "    sns.distplot(dataset[column])\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 8))\n",
    "ax = sns.countplot(dataset['sales'])\n",
    "plt.title('sales')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 8))\n",
    "ax = sns.countplot(dataset['salary'])\n",
    "plt.title('salary')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проведем анализ влияния признаков на целевую переменную:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_distribution(df, var, target, yl=4, **kwargs):\n",
    "    row = kwargs.get('row', None)\n",
    "    col = kwargs.get('col', None)\n",
    "    facet = sns.FacetGrid(df, hue=target, aspect=4, row=row, col=col)\n",
    "    facet.map(sns.kdeplot, var, shade=True)\n",
    "    facet.set(xlim=(0, df[var].max()), ylim=(0, yl))\n",
    "    facet.add_legend()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(dataset, 'satisfaction_level', 'left')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из распределения __satisfaction_level__ видно, что люди часто увольняются, когда уровень удовлетворение мал, в тоже время небольшая часть людей увольняется и при высоком уровне - возможно находят лучше или их переманивают."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(dataset, 'last_evaluation', 'left')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из распределения __last_evaluation__ видно люди уходят на другое место если их долго не повышают (есть 2 горба - первый это возле максимального времени пересмотра, второй примерно в середине)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(dataset, 'number_project', 'left', yl=1.5)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Четких закономерностей не видно."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(dataset, 'average_montly_hours', 'left', yl=0.015)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Четких закономерностей не видно: увольняются обычные сотрудники с ~140 часов/мес и в такой же степени трудоголики с 250+ часов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(dataset, 'time_spend_company', 'left', yl=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из распределения __time_spend_company__ видно, что вероятность, того человек уволится растет в течении времени его работы в компании."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 4. Закономерности, \"инсайты\", особенности данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Построим scatter plot, предварительно преобразуя категориальные признаки с помощью LabelEncoder"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.set(style=\"ticks\")\n",
    "\n",
    "labeled_dataset = dataset.copy()\n",
    "\n",
    "salary_le = preprocessing.LabelEncoder()\n",
    "sales_le = preprocessing.LabelEncoder()\n",
    "\n",
    "labeled_dataset['salary'] = salary_le.fit_transform(labeled_dataset['salary'])\n",
    "labeled_dataset['sales'] = sales_le.fit_transform(labeled_dataset['sales'])\n",
    "\n",
    "sns.pairplot(labeled_dataset, hue=\"left\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Рассмотрим более подробно признаки:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12, 10))\n",
    "\n",
    "sns.factorplot(y=\"satisfaction_level\",x=\"left\",data=dataset,kind=\"box\")\n",
    "sns.factorplot(y=\"last_evaluation\", x=\"left\", data=dataset, kind=\"box\")\n",
    "sns.factorplot(y=\"number_project\", x=\"left\", data=dataset, kind=\"box\")\n",
    "sns.factorplot(y=\"average_montly_hours\", x=\"left\", data=dataset, kind=\"box\")\n",
    "sns.factorplot(y=\"time_spend_company\", x=\"left\", data=dataset, kind=\"box\")\n",
    "sns.factorplot(y=\"sales\", x=\"left\", data=dataset, kind=\"box\")\n",
    "sns.factorplot(y=\"salary\", x=\"left\", data=dataset, kind=\"box\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(labeled_dataset, 'sales', 'left', yl=0.4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_distribution(labeled_dataset, 'salary', 'left', yl=3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Инсайты:\n",
    "\n",
    "    - более склонны к увольнениям люди с большим количеством рабочих часов в месяц и уровнем удовлетворенности\n",
    "    ниже среднего;\n",
    "    - люди, работающие больше 7 лет, практически не увольняются, вне зависимости от уровня удовлетворенности;\n",
    "    - среди людей с низким уровнем компенсации высокая текучка;\n",
    "    - высокое количество увольнений в областях: HR, Accounting, Technical (20% - 30% от всех сотрудников).\n",
    "    \n",
    "Общие мысли:\n",
    "\n",
    "    Важными факторами являются:\n",
    "        - низкая зарплата;\n",
    "        - определенный департамент;\n",
    "        - низкий уровень удовлетворенности;\n",
    "        - большие переработки;\n",
    "        - большое количество проектов.\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 7. Предобработка данных "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "В качестве кодирование категориальных переменных будет использовать __LabelEncoder__.\n",
    "\n",
    "Для нормализации датасета используем __StandardScaler__."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "salary_le = preprocessing.LabelEncoder()\n",
    "sales_le = preprocessing.LabelEncoder()\n",
    "\n",
    "dataset['salary'] = salary_le.fit_transform(dataset['salary'])\n",
    "dataset['sales'] = sales_le.fit_transform(dataset['sales'])\n",
    "\n",
    "# может пригодиться\n",
    "Norma = preprocessing.StandardScaler()\n",
    "dataset_norm = Norma.fit_transform(dataset)\n",
    "\n",
    "dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поделим датасет на тренировочную и тестовые выборки (используем ненормированный датасет).<"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset_v1 = dataset.copy()\n",
    "y = dataset_v1['left'].values\n",
    "dataset_v1.drop(['left'], axis=1, inplace=True)\n",
    "X = dataset_v1.copy()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть. 5. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве метрики выберем основной __ROC_AUC__, дополнительно будем смотреть __confusion_matrix__.\n",
    "\n",
    "Задача классификации - предположим, что нас интересуют вероятности (позволяет более точно определять степень уверенности модели). \n",
    "\n",
    "Наша целевая переменная не очень сбалансирована, поэтому использовать __accuracy__ выглядит не очень хорошей затеей. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть. 6. Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве пробных моделей проведем отбор с дефолтными параметрами среди следующих моделей:\n",
    "\n",
    "    - LogisticRegression;\n",
    "    - RandomForest;\n",
    "    - XGBoost;\n",
    "    - CATboost;\n",
    "    \n",
    "Все вышеописанные модели вполне применимы для задач бинарной классификации и можно использовать ненормализованный датасет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_lm = LogisticRegression(random_state=42)\n",
    "preds_lm = cross_val_predict(clf_lm, X, y, cv=5)\n",
    "print('LogisticRegression: %s ROC AUC' % round(roc_auc_score(y, preds_lm), 4))\n",
    "\n",
    "clf_rf = RandomForestClassifier(random_state=42)\n",
    "preds_rf = cross_val_predict(clf_rf, X, y, cv=5)\n",
    "print('RandomForest      : %s ROC AUC' % round(roc_auc_score(y, preds_rf), 4))\n",
    "\n",
    "clf_xgb = xgb.XGBClassifier(random_state=42)\n",
    "preds_xgb = cross_val_predict(clf_xgb, X, y, cv=5)\n",
    "print('XGBoost           : %s ROC AUC' % round(roc_auc_score(y, preds_xgb), 4))\n",
    "\n",
    "clf_cat = cat.CatBoostClassifier(random_seed=42)\n",
    "preds_cat = cross_val_predict(clf_cat, X, y, cv=5)\n",
    "print('CATBoost          : %s ROC AUC' % round(roc_auc_score(y, preds_cat), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "RandomForest в этой задаче показывает впечатляющие результаты. Построим ROC кривую для полученных результатов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fpr_rf, tpr_rf, threshold = roc_curve(y, preds_rf)\n",
    "roc_auc_rf = auc(fpr_rf, tpr_rf)\n",
    "fpr_xgb, tpr_xgb, threshold = roc_curve(y, preds_xgb)\n",
    "roc_auc_xgb = auc(fpr_xgb, tpr_xgb)\n",
    "fpr_lr, tpr_lr, threshold = roc_curve(y, preds_lm)\n",
    "roc_auc_lr = auc(fpr_lr, tpr_lr)\n",
    "\n",
    "plt.figure(figsize=(12, 8))\n",
    "plt.title('Receiver Operating Characteristic')\n",
    "plt.plot(fpr_rf, tpr_rf, 'b', label = 'AUC RF = %0.2f' % roc_auc_rf)\n",
    "plt.plot(fpr_xgb, tpr_xgb, 'g', label = 'AUC XGB = %0.2f' % roc_auc_xgb)\n",
    "plt.plot(fpr_lr, tpr_lr, 'r', label = 'AUC LR = %0.2f' % roc_auc_lr)\n",
    "plt.legend(loc = 'lower right')\n",
    "plt.plot([0, 1], [0, 1],'r--')\n",
    "plt.xlim([0, 1])\n",
    "plt.ylim([0, 1])\n",
    "plt.ylabel('True Positive Rate')\n",
    "plt.xlabel('False Positive Rate')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fig_size = [8, 6]\n",
    "plt.rcParams[\"figure.figsize\"] = fig_size\n",
    "def plt_matrix(c_matrix, names, title, normalize=False):\n",
    "    np.set_printoptions(precision=2)\n",
    "    plt.figure()\n",
    "    plt.imshow(c_matrix, interpolation='nearest', cmap=plt.cm.Blues)\n",
    "    plt.title(title)\n",
    "    plt.colorbar()\n",
    "    tick_marks = np.arange(len(names))\n",
    "    plt.xticks(tick_marks, names)\n",
    "    plt.yticks(tick_marks, names)\n",
    "    fmt = '.2f' if normalize else 'd'\n",
    "    thresh = c_matrix.max() / 2.\n",
    "    for i, j in itertools.product(range(c_matrix.shape[0]), range(c_matrix.shape[1])):\n",
    "        plt.text(j, i, format(c_matrix[i, j], fmt),\n",
    "                 horizontalalignment=\"center\",\n",
    "                 color=\"white\" if c_matrix[i, j] > thresh else \"black\")\n",
    "    plt.tight_layout()\n",
    "    plt.ylabel('True label')\n",
    "    plt.xlabel('Predicted label')  \n",
    "\n",
    "class_names = ['0', 'left']\n",
    "baseline_matrix = confusion_matrix(y, preds_rf)\n",
    "plt_matrix(baseline_matrix, class_names, 'Random Forest baseline')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 8. Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проведем поиск оптимальных параметров, посмотрим, получится ли улучшить модель.\n",
    "\n",
    "Сделаем это следующим образом:\n",
    "\n",
    "    1. Зафиксируем random_seed (для воспроизведения и равных условий разных моделей);\n",
    "    2. Будем использовать Stratified разбиения (target не сбалансирован);\n",
    "        - у нас 15000 записей, будешь разбивать на 5 фолдов.\n",
    "    3. Подбирать параметры моделей будем с помощью модуля hyperopt.\n",
    "    4. Описание параметров для тюнинга:\n",
    "        - n_estimators - количество деревьев в лесу;\n",
    "        - max_features - количество признаков, используемых для разделения;\n",
    "        - max_depth    - глубина дерева;\n",
    "        - class_weight - вес классов.\n",
    "        Возможно увеличение количество параметров для тюнинга, но вышеописанные параметры являются наиболее \n",
    "        важными для модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Выберем отложенную выборку, на ней мы проверим финальный результат модели:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_tuning, X_test, y_tuning, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)\n",
    "print(X_tuning.shape, X_test.shape, y_tuning.shape, y_test.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Попробуем сначала class_weight с дефолтными параметрами:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for weight in range(1, 20):\n",
    "    clf_rf = RandomForestClassifier(class_weight={0: 1, 1: weight}, random_state=42)\n",
    "    preds_rf = cross_val_predict(clf_rf, X_tuning, y_tuning, cv=5)\n",
    "    print('RandomForest   %s   : %s ROC AUC' % (weight, round(roc_auc_score(y_tuning, preds_rf), 4)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Вес для значений таргета 1 равный 1 является лучшим. Пока остановимся на нем."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Перейдем к затюниванию модели:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# наиболее простой вариант использования модуля hyperopt\n",
    "# стоит обратить внимание на факт, что модуль делает 20 случайных подборов параметров до начала настройки.\n",
    "def hyperopt_train_test(hpparams):\n",
    "    \n",
    "    # глобальный счетчик итераций для удобства и вывода информации.\n",
    "    global counter, result\n",
    "    counter += 1\n",
    "\n",
    "    # словарь подбираемых параметров\n",
    "    params_est = {\n",
    "        'n_estimators': int(hpparams['n_estimators']),\n",
    "        'max_features': hpparams['max_features'],\n",
    "        'max_depth': int(hpparams['max_depth']),\n",
    "        'random_state': 42,\n",
    "        'n_jobs': 8,\n",
    "        'class_weight': {0: 1, 1: 1},\n",
    "      }\n",
    "\n",
    "    # код обучения и получения результата метрики:\n",
    "    clf_rf = RandomForestClassifier(**params_est)\n",
    "    preds_rf = cross_val_predict(clf_rf, X_tuning, y_tuning, cv=5)\n",
    "    result_cur = round(roc_auc_score(y_tuning, preds_rf), 6)\n",
    "    \n",
    "    if result_cur > result:\n",
    "        print('Best iteration: %s, roc_auc: %s, params: %s' % (counter, result_cur, hpparams))\n",
    "        result = result_cur\n",
    "    return result_cur\n",
    "\n",
    "# пространство для поиска параметров\n",
    "# более детально можно ознакомится в документации.\n",
    "space4dt = {\n",
    "    'n_estimators': hp.quniform('n_estimators', 10, 100, 10),\n",
    "    'max_features': hp.quniform('max_features', 0.1, 1, 0.1),\n",
    "    'max_depth': hp.quniform('max_depth', 6, 20, 1),\n",
    "}\n",
    "\n",
    "def f(params):\n",
    "    metrics = hyperopt_train_test(params)\n",
    "    return {'loss': -metrics, 'status': STATUS_OK}\n",
    "\n",
    "\n",
    "trials = Trials()\n",
    "counter, result = 0, 0\n",
    "\n",
    "best = fmin(f, space4dt, algo=tpe.suggest, max_evals=100, trials=None)\n",
    "print('best: ', best)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Лучшие параметры (мы уперлись в 2 границы, значит улучшать еще есть куда, даже для этих параметров):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_rf = RandomForestClassifier(max_depth=20, max_features=0.5, n_estimators=100, random_state=42)\n",
    "preds_rf = cross_val_predict(clf_rf, X, y, cv=5)\n",
    "print('RandomForest      : %s ROC AUC' % round(roc_auc_score(y, preds_rf), 4))\n",
    "\n",
    "\n",
    "clf_rf = RandomForestClassifier(max_depth=20, max_features=0.5, n_estimators=100, random_state=42)\n",
    "preds_rf = clf_rf.fit(X_tuning, y_tuning)\n",
    "\n",
    "# отложенная выборка\n",
    "print('RandomForest test : %s ROC AUC' % round(roc_auc_score(y_test, clf_rf.predict(X_test)), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для желающих - можно расширить пространство признаков и их значений. Будете служить богу вычислений :)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 6. Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Итак, логически (поэтому и не совпадают номера глав) мы подошли генерации фичей (признаков). \n",
    "\n",
    "Предлагаю попробовать автоматическую генерацию линейных комбинаций признаков. Можно сгенерировать на первый вгляд логичные признаки, но все они попадут в наше поле зрения при генерации.\n",
    "\n",
    "Что мы имеем сейчас:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сгенерируем много много признаков:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset_generated = dataset.copy()\n",
    "print('\\nBefore transformation: ', dataset_generated.shape)\n",
    "columns = [\n",
    "    'satisfaction_level', 'last_evaluation', 'number_project', 'average_montly_hours',\n",
    "    'time_spend_company', 'salary'\n",
    "]\n",
    "for i1, col1 in enumerate(columns):\n",
    "    for i2, col2 in enumerate(columns):\n",
    "        if col1 == col2:\n",
    "            dataset_generated['%s_%s_0' % (col1, col2)] = np.log(X[col1] + 1)     \n",
    "        \n",
    "        dataset_generated['%s_%s_1' % (col1, col2)] = X[col1] / (X[col2] + 1)\n",
    "        dataset_generated['%s_%s_2' % (col1, col2)] = X[col1] * X[col2]\n",
    "print('\\nAfter transformation: ', dataset_generated.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь проведем отбор признаков. Для этого воспользуемся или самописным жадным отбором или уже написанным для нас в пакете __mlxtend__. Используем вот такой вариант отбора - SequentialFeatureSelector (есть несколько вариантов, можно почитать документацию по пакету). Наш вариант добавляет по одному признаку в датасет, и выбирает лучшую комбинацию по достижению максимального количества признаков. Для нашей задачи давайте установим максимальное значение признаков равное 12 и минимальное 1."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_sfs = dataset_generated['left'].values\n",
    "dataset_generated.drop(['left'], axis=1, inplace=True)\n",
    "X_sfs = dataset_generated.copy()\n",
    "\n",
    "model = RandomForestClassifier(random_state=42)\n",
    "sfs1 = SFS(model, k_features=(1, 12), forward=True, floating=False,\n",
    "           verbose=2, scoring='roc_auc', cv=3, n_jobs=-1)\n",
    "sfs1 = sfs1.fit(X_sfs.as_matrix(), y_sfs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Results:')\n",
    "print(sfs1.k_feature_idx_)\n",
    "print(sfs1.k_score_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Собирем датасет только из отобранных фич:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset_best_features = pd.DataFrame()\n",
    "columns = []\n",
    "for elem in sfs1.k_feature_idx_:\n",
    "    dataset_best_features[dataset_generated.columns[elem]] = dataset_generated[dataset_generated.columns[elem]]\n",
    "dataset_best_features['left'] = y_sfs\n",
    "dataset_best_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Попробуем еще раз затюнить параметры модели для полученного датасета:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_best = dataset_best_features['left'].values\n",
    "dataset_best_features.drop(['left'], axis=1, inplace=True)\n",
    "X_best = dataset_best_features.copy()\n",
    "\n",
    "X_tuning_b, X_test_b, y_tuning_b, y_test_b = train_test_split(\n",
    "    X_best, y_best, test_size=0.2, random_state=42, stratify=y)\n",
    "print(X_tuning.shape, X_test.shape, y_tuning.shape, y_test.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_rf = RandomForestClassifier(random_state=42)\n",
    "preds_rf = cross_val_predict(clf_rf, X_best, y_best, cv=5)\n",
    "print('RandomForest      : %s ROC AUC' % round(roc_auc_score(y_best, preds_rf), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видим, добавление и отбор признаков в нашем случае не помог, значение целевой метрики уходшилось, попробуем исправить ситуацию тюнингом параметров модели:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# наиболее простой вариант использования модуля hyperopt\n",
    "# стоит обратить внимание на факт, что модуль делает 20 случайных подборов параметров до начала настройки.\n",
    "def hyperopt_train_test(hpparams):\n",
    "    \n",
    "    # глобальный счетчик итераций для удобства и вывода информации.\n",
    "    global counter, result\n",
    "    counter += 1\n",
    "\n",
    "    # словарь подбираемых параметров\n",
    "    params_est = {\n",
    "        'n_estimators': int(hpparams['n_estimators']),\n",
    "        'max_features': hpparams['max_features'],\n",
    "        'max_depth': int(hpparams['max_depth']),\n",
    "        'random_state': 42,\n",
    "        'n_jobs': 8,\n",
    "        'class_weight': {0: 1, 1: 1},\n",
    "      }\n",
    "\n",
    "    # код обучения и получения результата метрики:\n",
    "    clf_rf = RandomForestClassifier(**params_est)\n",
    "    preds_rf = cross_val_predict(clf_rf, X_best, y_best, cv=5)\n",
    "    result_cur = round(roc_auc_score(y_best, preds_rf), 6)\n",
    "    \n",
    "    if result_cur > result:\n",
    "        print('Best iteration: %s, roc_auc: %s, params: %s' % (counter, result_cur, hpparams))\n",
    "        result = result_cur\n",
    "    return result_cur\n",
    "\n",
    "# пространство для поиска параметров\n",
    "# более детально можно ознакомится в документации.\n",
    "space4dt = {\n",
    "    'n_estimators': hp.quniform('n_estimators', 10, 200, 10),\n",
    "    'max_features': hp.quniform('max_features', 0.1, 1, 0.1),\n",
    "    'max_depth': hp.quniform('max_depth', 6, 24, 1),\n",
    "}\n",
    "\n",
    "def f(params):\n",
    "    metrics = hyperopt_train_test(params)\n",
    "    return {'loss': -metrics, 'status': STATUS_OK}\n",
    "\n",
    "\n",
    "trials = Trials()\n",
    "counter, result = 0, 0\n",
    "\n",
    "best = fmin(f, space4dt, algo=tpe.suggest, max_evals=200, trials=None)\n",
    "print('best: ', best)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_rf = RandomForestClassifier(max_depth=22, max_features=0.8, n_estimators=110, random_state=42)\n",
    "clf_rf.fit(X_tuning_b, y_tuning_b)\n",
    "\n",
    "# отложенная выборка\n",
    "print('RandomForest test : %s ROC AUC' % round(roc_auc_score(y_test_b, clf_rf.predict(X_test_b)), 4))\n",
    "\n",
    "clf_rf = RandomForestClassifier(max_depth=22, max_features=0.8, n_estimators=110, random_state=42)\n",
    "preds_rf = cross_val_predict(clf_rf, X_best, y_best, cv=5)\n",
    "print('RandomForest      : %s ROC AUC' % round(roc_auc_score(y_best, preds_rf), 4))\n",
    "\n",
    "clf_rf = RandomForestClassifier(max_depth=22, max_features=0.8, n_estimators=110, random_state=42)\n",
    "preds_rf = cross_val_predict(clf_rf, X_best, y_best, cv=5)\n",
    "print('RandomForest      : %s ROC AUC' % round(roc_auc_score(y_best, preds_rf), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Тюнинг модели улучшил результат, возможно нужно большее пространство параметров и больше степеней свободы (добавить параметры модели). Результаты получились на исходном датасете с улучшенными параметрами __hyperopt__:\n",
    "\n",
    "    RandomForest      : 0.9859 ROC AUC (исходный датасет);\n",
    "    RandomForest      : 0.986 ROC AUC (переработанные признаки в датасете);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 8. Построение кривых валидации и обучения "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,\n",
    "                        n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):\n",
    "    plt.figure()\n",
    "    plt.title(title)\n",
    "    if ylim is not None:\n",
    "        plt.ylim(*ylim)\n",
    "    plt.xlabel(\"Training examples\")\n",
    "    plt.ylabel(\"Score\")\n",
    "    train_sizes, train_scores, test_scores = learning_curve(\n",
    "        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)\n",
    "    train_scores_mean = np.mean(train_scores, axis=1)\n",
    "    train_scores_std = np.std(train_scores, axis=1)\n",
    "    test_scores_mean = np.mean(test_scores, axis=1)\n",
    "    test_scores_std = np.std(test_scores, axis=1)\n",
    "    plt.grid()\n",
    "\n",
    "    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,\n",
    "                     train_scores_mean + train_scores_std, alpha=0.1,\n",
    "                     color=\"r\")\n",
    "    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,\n",
    "                     test_scores_mean + test_scores_std, alpha=0.1, color=\"g\")\n",
    "    plt.plot(train_sizes, train_scores_mean, 'o-', color=\"r\",\n",
    "             label=\"Training score\")\n",
    "    plt.plot(train_sizes, test_scores_mean, 'o-', color=\"g\",\n",
    "             label=\"Cross-validation score\")\n",
    "\n",
    "    plt.legend(loc=\"best\")\n",
    "    return plt\n",
    "\n",
    "title = \"RandomForest default\"\n",
    "cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)\n",
    "estimator = RandomForestClassifier(random_state=42)\n",
    "plot_learning_curve(estimator, title, X_best, y, ylim=(0.7, 1.01), cv=cv, n_jobs=8)\n",
    "\n",
    "title = \"RandomForest tuned\"\n",
    "cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)\n",
    "estimator = RandomForestClassifier(max_depth=20, max_features=0.5, n_estimators=100, random_state=42)\n",
    "plot_learning_curve(estimator, title, X_best, y, (0.7, 1.01), cv=cv, n_jobs=8)\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Модель достаточно хорошо сходится, хотя еще данные бы не помешали."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 9. Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Проверим __confusion_matrix__ для новых параметров:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "clf_rf = RandomForestClassifier(max_depth=20, max_features=0.5, n_estimators=100, random_state=42)\n",
    "preds_rf_tuned = cross_val_predict(clf_rf, X_best, y, cv=5)\n",
    "tuned_matrix = confusion_matrix(y, preds_rf_tuned)\n",
    "\n",
    "clf_rf = RandomForestClassifier(random_state=42)\n",
    "preds_rf_baseline = cross_val_predict(clf_rf, X, y, cv=5)\n",
    "baseline_matrix = confusion_matrix(y, preds_rf_baseline)\n",
    "\n",
    "plt_matrix(baseline_matrix, class_names, 'Random Forest baseline')\n",
    "plt_matrix(tuned_matrix, class_names, 'Random Forest tuned')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как видно выше по проекту, отложенной выборкой мы несколько раз пользовались для оценки качества модели."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 10. Оценка модели с описанием выбранной метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Выбранная метрика ROC_AUC близка к 1. Наша модель практически не ошибается и уверена в своих вероятностях. Как видно из confusion matrix в пункте 9. Модель неправильно идентифицирует всего 123 человек. Accuracy модели > 99%."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 11. Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "1. Мы отобрали наиболее хорошо подходящую модель.\n",
    "2. Провели тюнинг фичей\n",
    "3. Попробовали сгенерировать фичи и сравнили модели между собой.\n",
    "4. Получили лучшую модель."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
